package edu.hawaii.duedates.userlibraryinfo;

import java.io.IOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Locale;
import org.xml.sax.SAXException;
import com.meterware.httpunit.GetMethodWebRequest;
import com.meterware.httpunit.HttpUnitOptions;
import com.meterware.httpunit.WebConversation;
import com.meterware.httpunit.WebForm;
import com.meterware.httpunit.WebLink;
import com.meterware.httpunit.WebRequest;
import com.meterware.httpunit.WebResponse;
import com.meterware.httpunit.WebTable;
import edu.hawaii.duedates.book.Book;
import edu.hawaii.duedates.library.LibraryType;
import edu.hawaii.duedates.user.User;


/**
 * The UserLibraryInfo Class implements the IUserLibraryInfo interface.  A user library info object
 * is comprised of a library, a user, and the validity of the request for information of this 
 * pair.  A UserLibraryInfo object has the behavior of requesting from the library the checked out
 * items for the user.
 * 
 * @author Ronn Reeves and Robin E. Raqueno
 */
public class UserLibraryInfo implements IUserLibraryInfo {
  
  /**  The collection of borrowed books.  */
  private List<Book> books;
  
  /** Active internet connection flag. */
  private boolean connectedToInternet = Boolean.TRUE;

  /**  The count of books per library.  */
  private List<Integer> counts;

  /**  The collection of libraries.  */
  private List<LibraryType> libraries;
  
  /** The report. */
  private final StringBuilder messageBuilder;

  /**  The collection of users.  */
  private List<User> users;

  /**  The validity of the request.  */
  private List<Boolean> validities;

  
  /**
   * Creates an empty collection of user library pairs.
   */
  public UserLibraryInfo() {
    this.users = new ArrayList<User>();
    this.libraries = new ArrayList<LibraryType>();
    this.books = new ArrayList<Book>();
    this.validities = new ArrayList<Boolean>();
    this.counts = new ArrayList<Integer>();
    this.messageBuilder = new StringBuilder();
  }


  /**
   * Add a user library pair to the UserLibraryInfo collection.
   * 
   * @param user  The user for whom the info is being requested.
   * @param library  The library from which the info is being to requested.
   */
  public void add(User user, LibraryType library) {
    this.users.add(user);
    this.libraries.add(library);
    this.validities.add(Boolean.FALSE);
  }

  
  /**
   * Sorts the books by the specified option.
   * 
   * @param option The sort option.
   * 
   * @return List&lt;Book&gt; The sorted list of books based on the sorting option.
   */
  List<Book> bookSort(String option) {
    List<Book> sortedBooks = new ArrayList<Book>();
    
    // Copy books in to sortedBooks array.
    for (Book book : books) {
      sortedBooks.add(book);
    }
    
    // If sortedBooks has one or fewer books, the list is sorted.
    if (sortedBooks.size() <= 1) {
      return sortedBooks;
    }

    // Otherwise, the books need to be sorted.
    for (int outerIndex = 1; outerIndex < sortedBooks.size(); outerIndex++) {
       
      Book val = sortedBooks.get(outerIndex);
      int innerIndex;
      for (innerIndex = outerIndex - 1; innerIndex > -1; innerIndex--) {
        Book temp = sortedBooks.get(innerIndex);
        if ("library".equals(option) && temp.compareByLibrary(val) <= 0) {
          break;
        }
        else if ("duedate".equals(option) && temp.compareByDate(val) <= 0) {
          break;
        }
        sortedBooks.set(innerIndex + 1, temp);
      }
      sortedBooks.set(innerIndex + 1, val);
    }
    return sortedBooks;
  }

  
  /**
   * Returns the current page response of this web conversation.
   * 
   * @param library The library the request is for.
   * @param conversation  This current web conversation.
   * @param response  The response information of this web conservation.
   * 
   * @return WebResponse  The current page response of the WebConversation.
   */
  private WebResponse checkSummaryPage(LibraryType library, WebConversation conversation,
    WebResponse response) {
    try {
      for (WebLink weblink : response.getLinks()) {
        if (weblink.getText().equals(library.getSummary())) {
          weblink.click();
        }
      }
      return conversation.getCurrentPage();
    }
    
    catch (IOException e) {
      return conversation.getCurrentPage();
    }
    
    catch (SAXException e) {
      return conversation.getCurrentPage();
    }
  }

  
  /**
   * Displays the checked out items information for each user and library.
   * 
   * @param sortOption The sort option (by library or due date).
   * @param withinDays The number of days until being due for the books to be displayed.
   * 
   * @return This report.
   */
  public String createReport(String sortOption, int withinDays) {
    if (connectedToInternet) {
      // Report the total number of items checked out.
      reportTotalItems();
  
      // If there are no books, return to caller.
      if (books.isEmpty()) {
        return this.messageBuilder.toString();
      }
  
      // Report the number of books checked out per library.
      reportAccountItems();
  
      // Otherwise, report the books based on the sorting and within options.
      reportColumnTitles(sortOption);
      reportItems(sortOption, withinDays);    
    }
    
    return this.messageBuilder.toString();
  }


  /**
   * Calculates the difference between today and this book's due date.
   * Returns 0 if the book is due today or overdue.
   * 
   * @param end The due date of the book.
   * @return difference The difference of days from today and the due date.
   */
  private long dateDiff(Calendar end) {
    Calendar today = Calendar.getInstance(Locale.US);

    // If the book is overdue return 0.
    if (end.before(today)) {
      return 0;
    }
    
    // Otherwise return the difference from today and the due date.
    long dif = (end.getTimeInMillis() - today.getTimeInMillis());
    return (dif / (1000L * 60L * 60L * 24L));
  }


  /**
   * Returns the collection of books for this user library info request.
   *
   * @return List&lt;Book&gt;  The collection of books.
   */
  public List<Book> getBooks() {
    return this.books;
  }


  /**
   * Returns this collection of library objects.
   *
   * @return List&lt;LibraryType&gt;  The library collection.
   */
  public List<LibraryType> getLibraries() {
    return this.libraries;
  }


  /**
   * Returns the current report.
   * 
   * @return String The current report.
   */
  public String getReport() {
    return this.messageBuilder.toString();
  }
  
  
  /**
   * Returns this collection of user objects.
   *
   * @return List&lt;User&gt;  The user collection.
   */
  public List<User> getUsers() {
    return this.users;
  }


  /**
   * Returns the validity of this user library colletion pair.  Returns true if the request is 
   * successful; false, otherwise.
   *
   * @return List&lt;Boolean&gt;  The validities of each user library pair.
   */
  public List<Boolean> getValidities() {
    return this.validities;
  }


  /**
   * Logs this user into this library's web site.
   * 
   * @param library The library the request is for.
   * @param user The user the request is for.
   * @param index The index for the library and user in this UserLibraryInfo object.
   * 
   * @return conversation The results page of the library's web site after logging in.
   */
  private WebConversation loginToLibrary(LibraryType library, User user, int index) {
    // Suppress any exceptions due to javascript.
    HttpUnitOptions.setExceptionsThrownOnScriptError(false);
    
    // Begin a new conversation.
    WebConversation conversation = new WebConversation();

    try {
      WebRequest request = new GetMethodWebRequest(library.getSiteUrl());
      WebResponse response = conversation.getResponse(request);

      WebLink link = null;
      if (!(library.getLink().equals(""))) {
        link = response.getLinkWith(library.getLink());
        link.click();
        response = conversation.getCurrentPage();
      }

      WebForm form = null;
      if ((library.getFormName().equals(""))) {
        for (WebForm webForm : response.getForms()) {
          if (webForm.hasParameterNamed(library.getUserFieldName())) {
            form = webForm;
          }
        }
      }

      else {
        form = response.getFormWithName(library.getFormName());
      }
      
      if ((form == null)) {
        this.validities.set(index, Boolean.FALSE);
      }
      
      else {
        form.setParameter(library.getUserFieldName(), user.getUserID());
        form.setParameter(library.getPasswordFieldName(), user.getPassword());
        form.submit();
        response = conversation.getCurrentPage();
      }

      if ((!(library.getErrorMessage().equals(""))) 
        && (response.getText().contains(library.getErrorMessage()))) {
        this.validities.set(index, Boolean.FALSE);
        conversation = null;
      }
    }

    catch (ConnectException e) {
      this.messageBuilder.append("\n>>> ERROR LOGGING IN TO WEBSITE.  "
        + "Site may be down due to maintenence.  Please try again later.");
      connectedToInternet = Boolean.FALSE;
      conversation = null;
    }
    
    catch (UnknownHostException e) {
      this.messageBuilder.append("\n>>> ERROR CONNECTING TO INTERNET.  "
        + "Please connect to the internet or check your internet connection.");
      connectedToInternet = Boolean.FALSE;
      conversation = null;
    }

    catch (Exception e) {
      this.validities.set(index, Boolean.FALSE);
      conversation = null;
    }
    
    return conversation;
  }
  
  
  /**
   * Returns true if this web response contained a table with checked out items.  Otherwise,
   * return false if their are not currently checked out items.
   * 
   * @param library The library the request is for.
   * @param response  The current web page for the current web conversation
   * @param idx The index of the library in this UserLibraryInfo object.
   * 
   * @return Boolean  True if the table had checked out items; otherwise, false. 
   */
  private Boolean processBooks(LibraryType library, WebResponse response, int idx) {
    boolean isProcessed = false;
    counts.add(0);
    try {
      for (WebTable table : response.getTables()) {
        if (table.getColumnCount() > 1) {
          isProcessed = parseHTMLTable(table, library, idx);
                    
          if (isProcessed) {
            break;
          }
        }

        else if (table.getColumnCount() == 1) {
          isProcessed = parseFlatTable(table, library, idx);
                    
          if (isProcessed) {
            break;
          }
        }
      }
      return isProcessed;
    }
    
    catch (Exception e) {
      this.validities.set(idx, Boolean.FALSE);
      return isProcessed;
    }
  }
  
  
  /**
   * Parses tables structured as a single column with cells separated by a | (pipe) symbol.
   * 
   * @param table  The current table being processed.
   * @param library The library the request is for.
   * @param idx The index of the library in this UserLibraryInfo object.
   * 
   * @return Boolean  True if the table had checked out items; otherwise, false. 
   */
  boolean parseFlatTable(WebTable table, LibraryType library, int idx) {
    boolean isProcessed = false;
    boolean isBookTable = false;

    if ((table.getCellAsText(0, 0).contains(library.getTitleColumn())) 
      && (table.getCellAsText(0, 0).contains(library.getDateColumn()))) {
      isBookTable = true;
    }

    if (isBookTable) {
      int count = 0;
      String delimiter = "\\|";
      String[] columns = table.getCellAsText(0, 0).split(delimiter);
      String title = "";
      String dueDate = "";
      for (int index = 0; index < columns.length; index++) {
        if (columns[index].contains(" /")) {
          title = columns[index].substring(0, columns[index].lastIndexOf('/')).trim();
          index += 3;
          dueDate = columns[index].trim();
          Book book = new Book(title, dueDate, library.getName());
          books.add(book);
          count++;
        }
      }
      isProcessed = true;
      counts.set(idx, count);
    }
    return isProcessed;
  }
  
  
  /**
   * Parses table structured with td cells.
   * 
   * @param table  The current table being processed.
   * @param library The library the request is for.
   * @param idx The index of the library in this UserLibraryInfo object.
   * 
   * @return Boolean  True if the table had checked out items; otherwise, false. 
   */
  boolean parseHTMLTable(WebTable table, LibraryType library,  int idx) {
    boolean isProcessed = false;
    boolean isBookTable = false;
    int titleCol = 0;
    int dateCol = 0;
    for (int col = 0; col < table.getColumnCount(); col++) {
      if (table.getCellAsText(0, col).equals(library.getTitleColumn())) {
        titleCol = col;
      }
      if (table.getCellAsText(0, col).equals(library.getDateColumn())) {
        dateCol = col;
      }
      if (titleCol != dateCol) {
        isBookTable = true;
      }
    }

    if (isBookTable) {
      int count = 0;
      for (int row = 1; row < table.getRowCount(); row++) {
        String title = "";
        String dueDate = "";
        for (int col = 0; col < table.getColumnCount(); col++) {
          if (col == titleCol) {
            title = table.getCellAsText(row, col).trim();
          }
          else if (col == dateCol) {
            dueDate = table.getCellAsText(row, col).trim();
          }
        }
        Book book = new Book(title, dueDate, library.getName());
        books.add(book);
        count++;
      }
      isProcessed = true;
      counts.set(idx, count);
    }
    
    return isProcessed;
  }

  
  /**
   * Displays the total number of items checked out per account.
   */
  void reportAccountItems() {
    for (int index = 0; index < libraries.size(); index++) {
      this.messageBuilder.append("\n" + counts.get(index) + " book(s) from the " 
        + libraries.get(index).getName() + " for User Account " + users.get(index).getUserID());
    }
  }
  
  
  /**
   * Displays the column headings based on the sort option.
   * 
   * @param sortOption The sort option (by library or due date).
   */
  private void reportColumnTitles(String sortOption) {
    this.messageBuilder.append("\n\n");
    String libraryLine = "---------------------------------------- ";
    String dateLine = "------------ ";
    String titleLine = 
      "---------------------------------------------------------------------------";

    if ("library".equals(sortOption)) {
      this.messageBuilder.append(String.format("%-40s %-12s %s\n", 
        "Library", "Due Date", "Book Title"));
      this.messageBuilder.append(libraryLine + dateLine + titleLine);
    }

    else {
      this.messageBuilder.append(String.format("%-12s %-40s %s\n", 
        "Due Date", "Library", "Book Title"));
      this.messageBuilder.append(dateLine + libraryLine + titleLine);
    }
  }
  
  
  /**
   * Displays the items checked out.
   * 
   * @param sortOption The sort option (by library or due date).
   * @param withinDays The number of days until being due for the books to be displayed.
   */
  private void reportItems(String sortOption, int withinDays) {
    int count = 0;

    List<Book> sortedBooks = this.bookSort(sortOption);
    for (Book book : sortedBooks) {
      Calendar bookDate = book.getDueDate();
      long dateDiff = this.dateDiff(bookDate);

      if ((dateDiff <= withinDays) || (withinDays == -1)) {
        if ("library".equals(sortOption)) {
          this.messageBuilder.append(String.format("\n%-40s %-12s %s", 
            book.getLibrary(), book.convertDate(), book.getTitle()));
        }

        else {
          this.messageBuilder.append(String.format("\n%-12s %-40s %s",
            book.convertDate(), book.getLibrary(), book.getTitle()));
        }
        count++;
      }
    }

    if (withinDays > -1) {
      this.messageBuilder.append("\n\n" + count + " book(s) due within " + withinDays + " days.");
    }
  }
  
  
  /**
   * Displays the total number of items checked out.
   */
  void reportTotalItems() {
    // Report the number of books checked out.
    this.messageBuilder.append("\nLIBRARY BOOKS REPORT");
    this.messageBuilder.append("\n==================================================");
    this.messageBuilder.append("\n" + books.size() + " Book(s) Checked Out\n");
  }
  
  
  /**
   * Requests the info from this library for this user.
   */
  public void requestInfo() {
    books = new ArrayList<Book>();
    counts = new ArrayList<Integer>();
    
    for (int index = 0; index < this.libraries.size(); index++) {
      LibraryType library = this.libraries.get(index);
      User user = this.users.get(index);
      String msgNoBorrowedItems = library.getNoBorrowedItemsMessage();
      this.validities.set(index, Boolean.TRUE);

      try {
        HttpUnitOptions.setExceptionsThrownOnScriptError(false);
        WebConversation conversation = this.loginToLibrary(library, user, index);
        
        if (!(conversation == null)) {
          WebResponse response = conversation.getCurrentPage();
        
          if (!(library.getSummary().equals(""))) {
            response = checkSummaryPage(library, conversation, response);
          }

          boolean processedBookTable = false;
          for (WebForm form : response.getForms()) {
            if (!(processedBookTable) && (!(msgNoBorrowedItems.equals(form.getText())))) {
              processedBookTable = processBooks(library, response, index);
            }
          }
        }
      }
      
      catch (Exception e) {
        this.validities.set(index, Boolean.FALSE);
      }
    }
  }


  /**
   * Sets the collection of books for this user library info request.
   * 
   * @param  books  The collection of books.
   */
  public void setBooks(List<Book> books) {
    this.books = books;
  }


  /**
   * Sets this collection of libraries from which the info is to be requested.
   * 
   * @param  libraries  The library collection.
   */
  public void setLibraries(List<LibraryType> libraries) {
    this.libraries = libraries;
  }


  /**
   * Sets this collection of users for which the info is to be requested.
   * 
   * @param  users  The user collection.
   */
  public void setUsers(List<User> users) {
    this.users = users;
  }


  /**
   * Sets the validity of this user library collection objects.
   * 
   * @param  validities  The validity status of each user library collection pair.
   */
  public void setValidities(List<Boolean> validities) {
    this.validities = validities;
  }


  /**
   * Returns the library name and user id of this User Library Info request.
   * 
   * @return String  The library name and user id.
   */
  public String toString() {
    StringBuffer buf = new StringBuffer();
    for (int index = 0; index < libraries.size(); ++index) {
      buf.append(this.libraries.get(index).getName());
      buf.append(" - ");
      buf.append(this.users.get(index).getUserID());
      buf.append('\n');
    }
    return buf.toString();
  }
}